AWS CloudFormationでVPCEndpoint + NLB + ALB + EC2の構成を作ってみる
こんにちは!AWS事業本部のおつまみです。
皆さん、AWS CloudFormationでVPCEndpoint + NLB + ALB + EC2の構成を作ってみたいなぁと思ったことはありますか?私はあります。
ちなみに2021年9月よりNLBのターゲットにALBを登録することができるようになってます。
今回同じようなシステム構成の案件に携わったので、せっかくなら自分でAWS CloudFormationを使って構築してみようと思い、やってみました。
ちなみにAWS CDKを使ったNLB + ALB + EC2の構成はこちらが参考になります。
実際にやってみた
構成図
ゴールはVPC AのEC2からVPC BのEC2に接続されることです。
- VPC AのEC2にはSession Manager経由で接続します。
- VPC BのEC2には事前にユーザデータで下記を設定しています。
- apacheをインストール
- ドキュメントルート配下(/var/www/html)にindex.htmlを作成し、「Hello World」を設定。
CloudFormationテンプレート
CloudFormationテンプレートはこのように3つに分けて作成しました。
VPCA.yml
AWSTemplateFormatVersion: "2010-09-09" Description: "VPC-A Template." Parameters: # ------------------------------------------------------------# # Common # ------------------------------------------------------------# Prefix: Type: String Default: "vpc-a" # ------------------------------------------------------------# # Network # ------------------------------------------------------------# VpcCidr: Type: String Default: "10.0.0.0/16" PrivateSubnetCidr: Type: String Default: "10.0.0.0/24" # ------------------------------------------------------------# # EC2 # ------------------------------------------------------------# EC2InstanceName: Type: String Default: "ec2" EC2InstanceAMI: Type: AWS::EC2::Image::Id Default: "ami-078296f82eb463377" # Amazon Linux 2 AMI (HVM) - Kernel 5.10, SSD Volume Type EC2InstanceInstanceType: Type: String Default: "t3.nano" EC2InstanceVolumeType: Type: String Default: "gp2" EC2InstanceVolumeSize: Type: String Default: "8" Resources: # ------------------------------------------------------------# # Network # ------------------------------------------------------------# Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidr EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${Prefix}-vpc PrivateSubnet: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PrivateSubnetCidr VpcId: !Ref Vpc AvailabilityZone: Fn::Select: - "0" - Fn::GetAZs: "" Tags: - Key: Name Value: !Sub ${Prefix}-private-subnet PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${Prefix}-table" PrivateSubnetTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable EC2SecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: "0.0.0.0/0" GroupName: !Sub "${Prefix}-sg" GroupDescription: "-" Tags: - Key: "Name" Value: !Sub "${Prefix}-sg" # ------------------------------------------------------------# # VPC Endpoint # ------------------------------------------------------------# SsmVpcEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub ${Prefix}-ssm-vpc-endpoint-sg GroupName: !Sub ${Prefix}-ssm-vpc-endpoint-sg VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 SourceSecurityGroupId: !Ref EC2SecurityGroup Tags: - Key: Name Value: !Sub ${Prefix}-ssm-vpc-endpoint-sg SsmVpcEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm VpcId: !Ref Vpc SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SsmVpcEndpointSecurityGroup SsmMessagesVpcEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg GroupName: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 SourceSecurityGroupId: !Ref EC2SecurityGroup Tags: - Key: Name Value: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg SsmMessagesVpcEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages VpcId: !Ref Vpc SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SsmMessagesVpcEndpointSecurityGroup # ------------------------------------------------------------# # Ec2InstanceProfile # ------------------------------------------------------------# Ec2Role: Type: AWS::IAM::Role Properties: RoleName: !Sub ${Prefix}-ec2-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore Ec2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Sub ${Prefix}-ec2-instance-profile Roles: - !Ref Ec2Role # ------------------------------------------------------------# # EC2Instance # ------------------------------------------------------------# EC2Instance: Type: "AWS::EC2::Instance" Properties: Tags: - Key: Name Value: !Sub "${Prefix}-${EC2InstanceName}" ImageId: !Ref EC2InstanceAMI InstanceType: !Ref EC2InstanceInstanceType IamInstanceProfile: !Ref Ec2InstanceProfile DisableApiTermination: false EbsOptimized: false BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: DeleteOnTermination: true VolumeType: !Ref EC2InstanceVolumeType VolumeSize: !Ref EC2InstanceVolumeSize SecurityGroupIds: - !Ref EC2SecurityGroup SubnetId: !Ref PrivateSubnet UserData: !Base64 | #! /bin/bash yum update -y # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: Vpc: Value: !Ref Vpc Export: Name: VpcA PrivateSubnet: Value: !Ref PrivateSubnet Export: Name: PrivateSubnet-VpcA EC2SecurityGroup: Value: !Ref EC2SecurityGroup Export: Name: EC2SecurityGroup-VpcA
VPCB.yml
AWSTemplateFormatVersion: "2010-09-09" Description: "VPC-B Template." Parameters: # ------------------------------------------------------------# # Common # ------------------------------------------------------------# Prefix: Type: String Default: "vpc-b" # ------------------------------------------------------------# # Network # ------------------------------------------------------------# VpcCidr: Type: String Default: "10.1.0.0/16" PrivateSubnetCidrA: Type: String Default: "10.1.0.0/24" PrivateSubnetCidrC: Type: String Default: "10.1.2.0/24" # ------------------------------------------------------------# # EC2 # ------------------------------------------------------------# EC2InstanceName: Type: String Default: "ec2" EC2InstanceAMI: Type: AWS::EC2::Image::Id Default: "ami-078296f82eb463377" # Amazon Linux 2 AMI (HVM) - Kernel 5.10, SSD Volume Type EC2InstanceInstanceType: Type: String Default: "t3.nano" EC2InstanceVolumeType: Type: String Default: "gp2" EC2InstanceVolumeSize: Type: String Default: "8" Resources: # ------------------------------------------------------------# # Network # ------------------------------------------------------------# Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidr EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${Prefix}-vpc PrivateSubnetA: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PrivateSubnetCidrA VpcId: !Ref Vpc AvailabilityZone: Fn::Select: - "0" - Fn::GetAZs: "" Tags: - Key: Name Value: !Sub ${Prefix}-private-subnet-a PrivateSubnetC: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PrivateSubnetCidrC VpcId: !Ref Vpc AvailabilityZone: Fn::Select: - "1" - Fn::GetAZs: "" Tags: - Key: Name Value: !Sub ${Prefix}-private-subnet-c PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Tags: - Key: Name Value: !Sub "${Prefix}-table" PrivateSubnetTableAssociationA: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnetA RouteTableId: !Ref PrivateRouteTable PrivateSubnetTableAssociationC: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnetC RouteTableId: !Ref PrivateRouteTable EC2SecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: "0.0.0.0/0" GroupName: !Sub "${Prefix}-ec2-sg" GroupDescription: "-" Tags: - Key: "Name" Value: !Sub "${Prefix}-ec2-sg" ALBSecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: "0.0.0.0/0" GroupName: !Sub "${Prefix}-alb-sg" GroupDescription: "-" Tags: - Key: "Name" Value: !Sub "${Prefix}-alb-sg" # ------------------------------------------------------------# # VPC Endpoint # ------------------------------------------------------------# S3VPCEndpoint: Type: "AWS::EC2::VPCEndpoint" Properties: RouteTableIds: - !Ref PrivateRouteTable ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3" VpcEndpointType: Gateway VpcId: !Ref Vpc # ------------------------------------------------------------# # EC2Instance # ------------------------------------------------------------# EC2Instance1: Type: "AWS::EC2::Instance" Properties: Tags: - Key: Name Value: !Sub "${Prefix}-${EC2InstanceName}" ImageId: !Ref EC2InstanceAMI InstanceType: !Ref EC2InstanceInstanceType DisableApiTermination: false EbsOptimized: false BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: DeleteOnTermination: true VolumeType: !Ref EC2InstanceVolumeType VolumeSize: !Ref EC2InstanceVolumeSize SecurityGroupIds: - !Ref EC2SecurityGroup SubnetId: !Ref PrivateSubnetA UserData: !Base64 | #! /bin/bash sudo yum update -y sudo yum -y install httpd sudo systemctl start httpd sudo systemctl enable httpd sudo echo "Hello World" >> /var/www/html/index.html EC2Instance2: Type: "AWS::EC2::Instance" Properties: Tags: - Key: Name Value: !Sub "${Prefix}-${EC2InstanceName}" ImageId: !Ref EC2InstanceAMI InstanceType: !Ref EC2InstanceInstanceType DisableApiTermination: false EbsOptimized: false BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: DeleteOnTermination: true VolumeType: !Ref EC2InstanceVolumeType VolumeSize: !Ref EC2InstanceVolumeSize SecurityGroupIds: - !Ref EC2SecurityGroup SubnetId: !Ref PrivateSubnetC UserData: !Base64 | #! /bin/bash sudo yum update -y sudo yum -y install httpd sudo systemctl start httpd sudo systemctl enable httpd sudo echo "Hello World" >> /var/www/html/index.html # ------------------------------------------------------------# # ALB # ------------------------------------------------------------# ALBTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: VpcId: !Ref Vpc Name: !Sub "${Prefix}-alb-tg" Protocol: HTTP Port: 80 Tags: - Key: Name Value: !Sub "${Prefix}-alb-tg" Targets: - Id: !Ref EC2Instance1 Port: 80 - Id: !Ref EC2Instance2 Port: 80 ALB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: IpAddressType: ipv4 Name: !Sub "${Prefix}-alb" Tags: - Key: Name Value: !Sub "${Prefix}-alb" Scheme: "internal" SecurityGroups: - !Ref ALBSecurityGroup Subnets: - !Ref PrivateSubnetA - !Ref PrivateSubnetC Type: application ALBListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - TargetGroupArn: !Ref ALBTargetGroup Type: forward LoadBalancerArn: !Ref ALB Port: 80 Protocol: HTTP # ------------------------------------------------------------# # NLB # ------------------------------------------------------------# NLBTargetGroup: DependsOn: - ALBListener Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: VpcId: !Ref Vpc Name: !Sub "${Prefix}-nlb-tg" Protocol: TCP Port: 80 Tags: - Key: Name Value: !Sub "${Prefix}-nlb-tg" Targets: - Id: !Ref ALB Port: 80 TargetType: alb NLB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Name: !Sub "${Prefix}-nlb" Tags: - Key: Name Value: !Sub "${Prefix}-nlb" Scheme: "internal" LoadBalancerAttributes: - Key: "deletion_protection.enabled" Value: false Subnets: - !Ref PrivateSubnetA - !Ref PrivateSubnetC Type: network NLBListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - TargetGroupArn: !Ref NLBTargetGroup Type: forward LoadBalancerArn: !Ref NLB Port: 80 Protocol: TCP # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: NLB: Value: !Ref NLB Export: Name: NLB
NLBVPCEndpoint.yml
AWSTemplateFormatVersion: "2010-09-09" Description: "NLBVPCEndpoint Template." Parameters: # ------------------------------------------------------------# # Common # ------------------------------------------------------------# Prefix: Type: String Default: "nlb-vpcendpoint" Resources: # ------------------------------------------------------------# # VPC Endpoint # ------------------------------------------------------------# NLBVPCEndpointService: Type: "AWS::EC2::VPCEndpointService" Properties: AcceptanceRequired: true NetworkLoadBalancerArns: - !ImportValue NLB NLBVPCEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub ${Prefix}-nlb-vpc-endpoint-sg GroupName: !Sub ${Prefix}-nlb-vpc-endpoint-sg VpcId: !ImportValue VpcA SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !ImportValue EC2SecurityGroup-VpcA Tags: - Key: Name Value: !Sub ${Prefix}-nlb-vpc-endpoint-sg NLBVPCEndpoint: DependsOn: - NLBVPCEndpointService Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface PrivateDnsEnabled: false ServiceName: !Sub com.amazonaws.vpce.${AWS::Region}.${NLBVPCEndpointService} VpcId: !ImportValue VpcA SubnetIds: - !ImportValue PrivateSubnet-VpcA SecurityGroupIds: - !Ref NLBVPCEndpointSecurityGroup
CloudFormation実行後には、エンドポイントサービスからエンドポイント接続を承認する必要があります。
そのため、下記の公式ドキュメントに記載の通りマネジメントコンソールもしくはAWS CLI経由で承認してあげましょう。
接続確認
VPC AのEC2にSession Manager経由で接続します。
下記のコマンド通りNLBのエンドポイントにcurlをして、VPC Bへの接続を確認します。
% curl <NLBのエンドポイント>
無事、Hello Worldと出力されました!
ハマったところ
ALBのヘルスチェックに403エラーで失敗する。
理由はVPC B側のEC2にindex.htmlを作成していないためでした。
そのため、ヘルスチェックをデフォルトの"/"にしている場合、apacheだと初期設定の/var/www/htmlにインデックスページを作成する必要があります。
NLBのターゲットグループ作成に失敗してしまう。
このようにCloudFormation実行が失敗しました。
理由はALB作成前にNLBのターゲットグループを作成しようとしているためでした。 そのため、NLBのターゲットグループにDependsOn属性で依存関係を明示してあげましょう。
NLBTargetGroup: DependsOn: - ALBListener Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: VpcId: !Ref Vpc Name: !Sub "${Prefix}-nlb-tg" Protocol: TCP Port: 80 Tags: - Key: Name Value: !Sub "${Prefix}-nlb-tg" Targets: - Id: !Ref ALB Port: 80 TargetType: alb
最後に
今回はAWS CloudFormationでVPCEndpoint + NLB + ALB + EC2の構成を作ってみました。
所々はまるポイントがあったため、同じような構成をAWS CloudFormationで構築する際にお役に立てれば幸いです。
最後までお読みいただきありがとうございました!
以上、おつまみ(@AWS11077)でした!